/**
 * @file virtio_disk.c
 * @date 23.04.30
 * @author xin
 * @brief qemu's virtio磁盘设备的驱动程序
 * @brief 使用qemu's mmio接口，相应的命令行如下
 * @brief qemu ... -drive file=fs.img,if=none,format=raw,id=x0 -device virtio-blk-device,drive=x0,bus=virtio-mmio-bus.0
 * @version 0.1
 * @status Stable
*/

#include "types.h"
#include "memlayout.h"
#include "virtio.h"
#include "defs.h"
#include "fs.h"
#include "buf.h"

// virtio mmio 中的寄存器r的地址
#define R(r) ((volatile uint32 *)(VIRTIO0 + (r)))

// 磁盘定义
static struct disk {
    struct virtq_desc *desc;        // 一系列DMA描述符的集合
    struct virtq_avail *avail;      // 驱动希望设备处理的描述符编号的环
    struct virtq_used *used;        // 设备已经完成处理的描述符编号的环

    char free[NUM];                 // 某个描述符是否已经空闲
    uint16 used_idx;                // 记录最新用过的used描述符索引

    // 跟踪使用过程中的信息
    // 中断到达时使用
    // 由链的第一个描述符索引
    struct 
    {
        struct buf *b;
        char status;
    }info[NUM];

    struct virtio_blk_req ops[NUM];  // 磁盘命令头部，为方便每个元素对应一个描述符
} disk;

void
virtio_disk_init(void)
{
    uint32 status = 0;

    // 检查VIRTIO MMIO设备相关信息
    if (*R(VIRTIO_MMIO_MAGIC_VALUE) != 0x74726976 ||
        *R(VIRTIO_MMIO_VERSION) != 2 ||
        *R(VIRTIO_MMIO_DEVICE_ID) != 2 ||
        *R(VIRTIO_MMIO_VENDOR_ID) != 0x554d4551) 
        panic("couldn't find virtio disk");

    // 重置设备状态：设置确认状态位、驱动状态位
    status |= VIRTIO_CONFIG_S_ACKNOWLEDGE | VIRTIO_CONFIG_S_DRIVER;

    // 确认特性
    uint64 feature = *R(VIRTIO_MMIO_DEVICE_FEATURES);
    feature &= ~(1 << VIRTIO_BLK_F_RO |
                 1 << VIRTIO_BLK_F_SCSI |
                 1 << VIRTIO_BLK_F_CONFIG_WCE |
                 1 << VIRTIO_BLK_F_MQ |
                 1 << VIRTIO_F_ANY_LAYOUT |
                 1 << VIRTIO_RING_F_EVENT_IDX |
                 1 << VIRTIO_RING_F_INDIRECT_DESC);
    *R(VIRTIO_MMIO_DEVICE_FEATURES) = feature;

    // 特性确认已完成
    status |= VIRTIO_CONFIG_S_FEATURES_OK;
    *R(VIRTIO_MMIO_STATUS) = status;

    // 再次读取status寄存器确保FEATURE_OK已经设置完毕
    status = *R(VIRTIO_MMIO_STATUS);
    if (!(status & VIRTIO_CONFIG_S_FEATURES_OK)) 
        panic("virtio disk FEATURE_OK unset");

    // 虚拟队列选择queue 0
    *R(VIRTIO_MMIO_QUEUE_SEL) = 0;

    // 确保queue 0没有正在使用
    if (*R(VIRTIO_MMIO_QUEUE_READY)) 
        panic("virtio disk should not be ready");

    // 检查queue 0最大size
    uint32 max = *R(VIRTIO_MMIO_QUEUE_NUM_MAX);
    if (max == 0) 
        panic("virtio disk has no queue 0");
    if (max < NUM) 
        panic("virtio disk max queue too short");

    // 为缓冲区描述符、空闲ring、used ring申请内存
    if (!(disk.desc = kalloc()) || !(disk.avail = kalloc()) || !(disk.used = kalloc()))
        panic("virtio disk kalloc");
    memset(disk.desc, 0, PGSIZE);
    memset(disk.avail, 0, PGSIZE);
    memset(disk.used, 0, PGSIZE);
    
    // 设置队列size
    *R(VIRTIO_MMIO_QUEUE_NUM) = NUM;

    // 填写相应物理地址(小端)
    *R(VIRTIO_MMIO_QUEUE_DESC_LOW) = (uint64)disk.desc;
    *R(VIRTIO_MMIO_QUEUE_DESC_HIGH) = (uint64)disk.desc >> 32;
    *R(VIRTIO_MMIO_DRIVER_DESC_LOW) = (uint64)disk.avail;
    *R(VIRTIO_MMIO_DRIVER_DESC_HIGH) = (uint64)disk.avail >> 32;
    *R(VIRTIO_MMIO_DEVICE_DESC_LOW) = (uint64)disk.used;
    *R(VIRTIO_MMIO_DEVICE_DESC_HIGH) = (uint64)disk.used >> 32;

    // 设置队列ready位
    *R(VIRTIO_MMIO_QUEUE_READY) = 0x1;

    // 设置所有描述符的free标志为1
    for (int i = 0; i < NUM; i++) 
        disk.free[i] = 1;

    // 设置状态为(驱动)ready
    status |= VIRTIO_CONFIG_S_DRIVER_OK;
    *R(VIRTIO_MMIO_STATUS) = status;
}

/**
 * @brief 找到一个空闲的缓冲区描述符，并设置为该描述符的free标志为1
 * @return 该描述符索引，若找不到空闲的返回-1
*/
static int
alloc_desc()
{
    for (int i = 0; i < NUM; i++)
    {
        if (disk.free[i])
        {
            disk.free[i] = 0;
            return i;
        }
    }
    return -1;
}

/**
 * @brief 释放一个缓冲区描述符，重置元素内容，并设置该描述符的free标志为1
 * @param i 该描述符索引
*/
static void
free_desc(int i)
{
    if (i >= NUM) panic("free_desc 1");
    if (disk.free[i]) panic("free_desc 2");
    disk.desc[i].addr = 0;
    disk.desc[i].len = 0;
    disk.desc[i].flags = 0;
    disk.desc[i].next = 0;
    disk.free[i] = 1;
    // 唤醒等待该缓冲区描述符的所有进程
    wakeup(&disk.free);
}

/**
 * @brief 从索引i开始，释放一条链上的描述符
 * @param i 开头的描述符索引
*/
static void
free_chain(int i)
{
    int flag;
    int nxt;
    while(1){
        flag = disk.desc[i].flags;
        nxt = disk.desc[i].next;
        free_desc(i);
        if(flag & VRING_DESC_F_NEXT)
            i = nxt;
        else
            break;
    }
}

/**
 * @brief 申请3个缓冲区描述符的内存，因为磁盘传输总是使用3个描述符
 * @param idx 待填充的描述符索引数组
 * @return 0:成功   1:失败
*/
static int 
alloc3_desc(int *idx)
{
    for (int i = 0; i < 3; i++)
    {
        idx[i] = alloc_desc();
        if (idx[i] < 0) {
            for (int j = 0; j < i; j++)
                free_desc(idx[j]);
            return -1;
        }
    }
    return 0;
}

void
virtio_disk_rw(struct buf *b, int write)
{
    uint64 sector = b->blockno * (BSIZE / 512);

    intr_off();

    // 申请的3个描述符分别用于
    //  类型/保留/扇区
    //  数据
    //  1字节状态结果
    int idx[3];
    while (1)
    {
        if (alloc3_desc(idx) == 0) 
            break;
        // 没有空余的3个缓冲区描述符则sleep
        sleep(&disk.free[0]);    
    }

    struct virtio_blk_req *buf0 = &disk.ops[idx[0]];
    if (write)
        buf0->type = VIRTIO_BLK_T_OUT;
    else
        buf0->type = VIRTIO_BLK_T_IN;
    
    buf0->reserved = 0;
    buf0->sector = sector;

    // 第一个描述符保留用于描述磁盘操作命令
    disk.desc[idx[0]].addr = (uint64)buf0;
    disk.desc[idx[0]].len = sizeof(struct virtio_blk_req);
    disk.desc[idx[0]].flags = VRING_DESC_F_NEXT;
    disk.desc[idx[0]].next = idx[1];

    // 第二个描述符用于数据
    disk.desc[idx[1]].addr = (uint64) b->data;
    disk.desc[idx[1]].len = BSIZE;
    if (write)  // 驱动写给设备读
        disk.desc[idx[1]].flags = 0;   
    else        // 设备写给驱动读
        disk.desc[idx[1]].flags = VRING_DESC_F_WRITE; 
    disk.desc[idx[1]].flags |= VRING_DESC_F_NEXT;
    disk.desc[idx[1]].next = idx[2];

    // 第三个描述符用于记录1字节状态结果
    disk.info[idx[0]].status = 0xff;    // 先把状态设置非0，若设备成功写入，则此处设置为0
    disk.desc[idx[2]].addr = (uint64) &disk.info[idx[0]].status;
    disk.desc[idx[2]].len = 1;
    disk.desc[idx[2]].flags = VRING_DESC_F_WRITE;   // 设备可写状态
    disk.desc[idx[2]].next = 0;

    b->disk = 1;
    disk.info[idx[0]].b = b;

    // 告诉设备3个描述符链的第一个索引已经可用了
    disk.avail->ring[disk.avail->idx % NUM] = idx[0];

    __sync_synchronize();

    disk.avail->idx ++;

    __sync_synchronize();

    *R(VIRTIO_MMIO_QUEUE_NOTIFY) = 0;

    while (b->disk == 1)
    {
        sleep(b);
    }
    
    disk.info[idx[0]].b = 0;
    free_chain(idx[0]);
    intr_on();
}

void
virtio_disk_intr()
{
    intr_off();
    *R(VIRTIO_MMIO_INTERRUPT_ACK) = *R(VIRTIO_MMIO_INTERRUPT_STATUS) & 0x3;

    __sync_synchronize();

    while(disk.used_idx != disk.used->idx) {
        __sync_synchronize();
        int id = disk.used->ring[disk.used_idx % NUM].id;

        if (disk.info[id].status != 0) 
            panic("virtio_disk_intr status");

        struct buf *b = disk.info[id].b;
        b->disk = 0;    // 磁盘使用buf完毕
        wakeup(b);

        disk.used_idx++;
    }
    intr_on();
}
